/* 
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.
*/

package org.odftoolkit.simple.table;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.odftoolkit.odfdom.dom.OdfDocumentNamespace;
import org.odftoolkit.odfdom.dom.attribute.office.OfficeValueTypeAttribute;
import org.odftoolkit.odfdom.dom.element.table.TableCoveredTableCellElement;
import org.odftoolkit.odfdom.dom.element.table.TableNamedExpressionsElement;
import org.odftoolkit.odfdom.dom.element.table.TableNamedRangeElement;
import org.odftoolkit.odfdom.dom.element.table.TableTableCellElement;
import org.odftoolkit.odfdom.dom.element.table.TableTableRowElement;
import org.odftoolkit.odfdom.pkg.OdfElement;
import org.odftoolkit.odfdom.pkg.OdfFileDom;
import org.odftoolkit.odfdom.pkg.OdfName;
import org.odftoolkit.odfdom.pkg.OdfXMLFactory;
import org.odftoolkit.simple.Document;
import org.odftoolkit.simple.SpreadsheetDocument;

/**
 * CellRange represent a rang of cells that are adjacent with each other
 * <p>
 * CellRange provides methods to get/set/modify the properties of cell range.
 */
public class CellRange {

	private int mnStartRow;
	private int mnStartColumn;
	private int mnEndRow;
	private int mnEndColumn;
	private String msCellRangeName;
	private Table maOwnerTable;
	private boolean mbSpreadsheet;

	/**
	 * Construct the instance of CellRange.
	 * @param table
	 * 					is the container table of this cell range.
	 * @param startColumn
	 * 					is the column index of the first cell in this cell range.
	 * @param startRow
	 * 					is the row index of the first cell in this cell range.
	 * @param endColumn
	 * 					is the column index of the last cell in this cell range.
	 * @param endRow
	 * 					is the row index of the last cell in this cell range.
	 */
	CellRange(Table table, int startColumn, int startRow, int endColumn, int endRow) {
		maOwnerTable = table;

		Document doc = (Document) ((OdfFileDom) maOwnerTable.getOdfElement().getOwnerDocument()).getDocument();
		if (doc instanceof SpreadsheetDocument) {
			mbSpreadsheet = true;
		}

		//the first cell is the covered cell, then the cell range should be enlarged
		//so that it can contains the complete cell
		//get the cell cover info
		mnStartColumn = startColumn;
		mnStartRow = startRow;
		mnEndColumn = endColumn;
		mnEndRow = endRow;
		List<CellCoverInfo> coverList = maOwnerTable.getCellCoverInfos(0, 0, endColumn, endRow);
		Cell cell;// = maOwnerTable.getOwnerCellByPosition(coverList, nStartColumn, nStartRow);
		for (int i = startColumn; i <= endColumn; i++) {
			cell = maOwnerTable.getOwnerCellByPosition(coverList, i, startRow);
			int rowIndex = cell.getRowIndex();
			int colIndex = cell.getColumnIndex();
			mnStartColumn = Math.min(mnStartColumn, colIndex);
			mnStartRow = Math.min(mnStartRow, rowIndex);
			mnEndColumn = Math.max(mnEndColumn, colIndex + cell.getColumnSpannedNumber() - 1);
			mnEndRow = Math.max(mnEndRow, rowIndex + cell.getRowSpannedNumber() - 1);
		}

		for (int i = startColumn; i <= endColumn; i++) {
			cell = maOwnerTable.getOwnerCellByPosition(coverList, i, endRow);
			int rowIndex = cell.getRowIndex();
			int colIndex = cell.getColumnIndex();
			mnStartColumn = Math.min(mnStartColumn, colIndex);
			mnStartRow = Math.min(mnStartRow, rowIndex);
			mnEndColumn = Math.max(mnEndColumn, colIndex + cell.getColumnSpannedNumber() - 1);
			mnEndRow = Math.max(mnEndRow, rowIndex + cell.getRowSpannedNumber() - 1);
		}

		for (int i = startRow + 1; i < endRow; i++) {
			cell = maOwnerTable.getOwnerCellByPosition(coverList, startColumn, i);
			int rowIndex = cell.getRowIndex();
			int colIndex = cell.getColumnIndex();
			mnStartColumn = Math.min(mnStartColumn, colIndex);
			mnStartRow = Math.min(mnStartRow, rowIndex);
			mnEndColumn = Math.max(mnEndColumn, colIndex + cell.getColumnSpannedNumber() - 1);
			mnEndRow = Math.max(mnEndRow, rowIndex + cell.getRowSpannedNumber() - 1);
		}

		for (int i = startRow + 1; i < endRow; i++) {
			cell = maOwnerTable.getOwnerCellByPosition(coverList, endColumn, i);
			int rowIndex = cell.getRowIndex();
			int colIndex = cell.getColumnIndex();
			mnStartColumn = Math.min(mnStartColumn, colIndex);
			mnStartRow = Math.min(mnStartRow, rowIndex);
			mnEndColumn = Math.max(mnEndColumn, colIndex + cell.getColumnSpannedNumber() - 1);
			mnEndRow = Math.max(mnEndRow, rowIndex + cell.getRowSpannedNumber() - 1);
		}
	}

	/**
	 * construct the empty cellRange
	 */
	CellRange() {
	}

	/**
	 * Merge the current cell range to one cell
	 */
	public void merge() {
		Cell firstCell = maOwnerTable.getCellByPosition(mnStartColumn, mnStartRow);

		//note: after merge, the cell row/column count might  be changed
		int rowCount = maOwnerTable.getRowCount();
		int colCount = maOwnerTable.getColumnCount();
		//if the cell range is the whole table, then merge it to a big cell
		//as to the spreadsheet document, it should still keep the original cell count,
		//rather than merge to a big cell.
		if (rowCount == (mnEndRow - mnStartRow + 1)
				&& colCount == (mnEndColumn - mnStartColumn + 1)
				&& !mbSpreadsheet) {
			if (firstCell.getOdfElement() instanceof TableTableCellElement) {
				TableTableCellElement firstCellElement = (TableTableCellElement) (firstCell.getOdfElement());
				firstCellElement.removeAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "number-columns-spanned");
				firstCellElement.removeAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "number-rows-spanned");
				firstCellElement.setOfficeValueTypeAttribute(OfficeValueTypeAttribute.Value.STRING.toString());
			}
			//just copy the text of the other cells to this first cell
			for (int i = mnStartRow; i < mnEndRow + 1; i++) {
				for (int j = mnStartColumn; j < mnEndColumn + 1; j++) {
					Cell cellBase = maOwnerTable.getCellByPosition(j, i);
					if (j != mnStartColumn || i != mnStartRow) {
						//copy the content of this cell to the first cell
						firstCell.appendContentFrom(cellBase);
					}
				}
			}
			maOwnerTable.removeRowsByIndex(1, maOwnerTable.getRowCount() - 1);
			maOwnerTable.removeColumnsByIndex(1, maOwnerTable.getColumnCount() - 1);
			Column firstColumn = maOwnerTable.getColumnByIndex(0);
			firstColumn.setWidth(maOwnerTable.getWidth());
			mnEndRow = mnStartRow;
			mnEndColumn = mnStartColumn;
			return;
		} //if the cell range covered all the table row, and the merged column > 1
		//the merged column can be removed
		else if (rowCount == (mnEndRow - mnStartRow + 1)
				&& colCount > (mnEndColumn - mnStartColumn + 1)
				&& (mnEndColumn - mnStartColumn) > 0) {
			//the first cell, set the span attribute
			if (firstCell.getOdfElement() instanceof TableTableCellElement) {
				TableTableCellElement firstCellElement = (TableTableCellElement) (firstCell.getOdfElement());
				firstCellElement.removeAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "number-columns-spanned");
				firstCellElement.setTableNumberRowsSpannedAttribute(Integer.valueOf(mnEndRow - mnStartRow + 1));
				firstCellElement.setOfficeValueTypeAttribute(OfficeValueTypeAttribute.Value.STRING.toString());
			}
			//the other cell, copy the content to first cell
			//if it is also in the first column of the cell range, set to the covered cell
			//other cell not in the first column will be removed when remove the column
			for (int i = mnStartRow; i < mnEndRow + 1; i++) {
				for (int j = mnStartColumn; j < mnEndColumn + 1; j++) {
					Cell cellBase = maOwnerTable.getCellByPosition(j, i);
					if (j != mnStartColumn || i != mnStartRow) {
						//append content to first cell
						firstCell.appendContentFrom(cellBase);
						//change the cell in the first column of cell range to covered cell
						if ((j == mnStartColumn) && (cellBase.getOdfElement() instanceof TableTableCellElement)) {
							//change the normal cell to be the covered cell
							TableTableCellElement firstColumnCell = (TableTableCellElement) cellBase.getOdfElement();
							TableCoveredTableCellElement coveredCell = (TableCoveredTableCellElement) OdfXMLFactory.newOdfElement(
									(OdfFileDom) firstColumnCell.getOwnerDocument(),
									OdfName.newName(OdfDocumentNamespace.TABLE, "covered-table-cell"));
							Row parentRow = cellBase.getTableRow();
							parentRow.getOdfElement().insertBefore(coveredCell, firstColumnCell);
							parentRow.getOdfElement().removeChild(firstColumnCell);
						}
					}
				}
			}
			List<Double> widthList = getCellRangeWidthList();
			double nCellRangeWidth = widthList.get(widthList.size() - 1) - widthList.get(0);
			maOwnerTable.removeColumnsByIndex(mnStartColumn + 1, mnEndColumn - mnStartColumn);
			Column firstColumn = maOwnerTable.getColumnByIndex(mnStartColumn);
			firstColumn.setWidth(nCellRangeWidth);
			mnEndColumn = mnStartColumn;
			return;
		} //if the cell range covered all the table column, the merged row can be removed
		else if (rowCount > (mnEndRow - mnStartRow + 1)
				&& colCount == (mnEndColumn - mnStartColumn + 1)
				&& (mnEndRow - mnStartRow) > 0) {
			//the first cell, set the span attribute
			if (firstCell.getOdfElement() instanceof TableTableCellElement) {
				TableTableCellElement firstCellElement = (TableTableCellElement) (firstCell.getOdfElement());
				firstCellElement.removeAttributeNS(OdfDocumentNamespace.TABLE.getUri(), "number-rows-spanned");
				firstCellElement.setTableNumberColumnsSpannedAttribute(Integer.valueOf(mnEndColumn - mnStartColumn + 1));
				firstCellElement.setOfficeValueTypeAttribute(OfficeValueTypeAttribute.Value.STRING.toString());
			}
			//the other cell, copy the content to first cell
			//if it is also in the first row of the cell range, set to the covered cell
			//other cell not in the first row will be removed when remove the row
			for (int i = mnStartRow; i < mnEndRow + 1; i++) {
				for (int j = mnStartColumn; j < mnEndColumn + 1; j++) {
					Cell cellBase = maOwnerTable.getCellByPosition(j, i);
					if (j != mnStartColumn || i != mnStartRow) {
						//append content to first cell
						firstCell.appendContentFrom(cellBase);
						//change the cell in the first row of cell range to covered cell
						if ((i == mnStartRow) && (cellBase.getOdfElement() instanceof TableTableCellElement)) {
							//change the normal cell to be the covered cell
							TableTableCellElement firstRowCell = (TableTableCellElement) cellBase.getOdfElement();
							TableCoveredTableCellElement coveredCell = (TableCoveredTableCellElement) OdfXMLFactory.newOdfElement(
									(OdfFileDom) firstRowCell.getOwnerDocument(),
									OdfName.newName(OdfDocumentNamespace.TABLE, "covered-table-cell"));
							Row parentRow = cellBase.getTableRow();
							parentRow.getOdfElement().insertBefore(coveredCell, firstRowCell);
							parentRow.getOdfElement().removeChild(firstRowCell);
						}
					}
				}
			}
			maOwnerTable.removeRowsByIndex(mnStartRow + 1, mnEndRow - mnStartRow);
			mnEndRow = mnStartRow;
			return;
		} //don't remove any row/column
		else {
			//first keep the column and row count in this cell range
			//the first cell, set the span attribute
			if (firstCell.getOdfElement() instanceof TableTableCellElement) {
				//first cell number columns repeated attribute may > 1.
				firstCell.splitRepeatedCells();
				TableTableCellElement firstCellElement = (TableTableCellElement) (firstCell.getOdfElement());
				firstCellElement.setTableNumberColumnsSpannedAttribute(mnEndColumn - mnStartColumn + 1);
				firstCellElement.setTableNumberRowsSpannedAttribute(mnEndRow - mnStartRow + 1);
			}
			//the other cell, set to the covered cell
			for (int i = mnStartRow; i < mnEndRow + 1; i++) {
				for (int j = mnStartColumn; j < mnEndColumn + 1; j++) {
					Cell cellBase = maOwnerTable.getCellByPosition(j, i);
					if (j != mnStartColumn || i != mnStartRow) {
						if (cellBase.getOdfElement() instanceof TableTableCellElement) {
							//change the normal cell to be the covered cell
							TableTableCellElement cell = (TableTableCellElement) cellBase.getOdfElement();
							TableCoveredTableCellElement coveredCell = (TableCoveredTableCellElement) OdfXMLFactory.newOdfElement(
									(OdfFileDom) cell.getOwnerDocument(),
									OdfName.newName(OdfDocumentNamespace.TABLE, "covered-table-cell"));

							TableTableRowElement parentRowEle = cellBase.getTableRow().getOdfElement();
							parentRowEle.insertBefore(coveredCell, cell);
							//copy the content of this cell to the first cell
							firstCell.appendContentFrom(cellBase);
							cellBase.removeContent();
							//set the table column repeated attribute
							int repeatedNum = cell.getTableNumberColumnsRepeatedAttribute();
							int num = (mnEndColumn - j + 1) - repeatedNum;
							if (num >= 0) {
								if(repeatedNum > 1){
									coveredCell.setTableNumberColumnsRepeatedAttribute(repeatedNum);
								}
								parentRowEle.removeChild(cell);
							} else {
								int tableNumberColumnsRepeatedValue = mnEndColumn - j + 1;
								if(tableNumberColumnsRepeatedValue > 1){
									coveredCell.setTableNumberColumnsRepeatedAttribute(tableNumberColumnsRepeatedValue);
								}
								cell.setTableNumberColumnsRepeatedAttribute(-num);
							}
						} else if (cellBase.getOdfElement() instanceof TableCoveredTableCellElement) {
							try {
								//copy the content of this cell to the first cell
								firstCell.appendContentFrom(cellBase);
								cellBase.removeContent();
							} catch (Exception e) {
								Logger.getLogger(CellRange.class.getName()).log(Level.SEVERE, e.getMessage(), e);
							}
						}
					}
				}
			}
		}
	}

	//vector store the x coordinate of each column which reference to the left start point of owner table
	//the returned value is all measured with "mm" unit
	private List<Double> getCellRangeWidthList() {
		List<Double> list = new ArrayList<Double>();
		Double length = Double.valueOf(0.0);
		for (int i = 0; i < maOwnerTable.getColumnCount() - 1; i++) {
			Column col = maOwnerTable.getColumnByIndex(i);
			int repeateNum = col.getColumnsRepeatedNumber();
			if (repeateNum == 1) {
				if (isColumnInCellRange(i)) {
					list.add(length);
				}
				length = Double.valueOf(length + col.getWidth());
			} else {
				for (int j = 0; j < repeateNum; j++) {
					if (isColumnInCellRange(i + j)) {
						list.add(length);
						length = Double.valueOf(length + col.getWidth());
					}
				}
				i += repeateNum - 1;
			}
		}
		//x coordinate of last column right point 
		list.add(length);
		return list;
	}

	//vector store the x coordinate of each will split column start point
	List<Double> getVeticalSplitCellRangeWidthList(int splitNum) {
		//get each cell in the cell range(the cell here means the real cell, not the covered cell)
		List<CellCoverInfo> coverList = maOwnerTable.getCellCoverInfos(mnStartColumn, mnStartRow, mnEndColumn, mnEndRow);
		//then get the real(uncovered) cell x coordinate
		List<Double> tmpList = new ArrayList<Double>();
		List<Double> widthList = getCellRangeWidthList();
		for (int i = mnStartColumn; i < mnEndColumn + 1; i++) {
			for (int j = mnStartRow; j < mnEndRow + 1; j++) {
				if (maOwnerTable.isCoveredCellInOwnerTable(coverList, i, j)) {
					continue;
				} else {
					//the real cell, record the x coordinate of the left point
					Double width = widthList.get(i - mnStartColumn);
					if (!tmpList.contains(width)) {
						tmpList.add(width);
					}
				}
			}
		}

		//last, reorder the tmpVector and split it to splitNum between each item
		Double[] widthArray = (Double[]) tmpList.toArray();
		Arrays.sort(widthArray);
		List<Double> rtnValues = new ArrayList<Double>();
		double colWidth;
		double unitWidth;
		rtnValues.add(widthArray[0]);
		for (int i = 1; i < widthArray.length; i++) {
			colWidth = Double.valueOf(widthArray[i] - widthArray[i - 1]);
			unitWidth = colWidth / splitNum;
			for (int j = 1; j < splitNum; j++) {
				double eachWidth = unitWidth * j + widthArray[i - 1];
				rtnValues.add(Double.valueOf(eachWidth));
			}
			rtnValues.add(widthArray[i]);
		}
		return rtnValues;
	}

	/**
	 * Get the name of the named cell range.
	 * 
	 * @return the name of the cell range
	 */
	public String getCellRangeName() {
		return msCellRangeName;
	}

	/**
	 * Set the name of the current cell range.
	 * 
	 * @param cellRangeName	the name that need to set
	 */
	public void setCellRangeName(String cellRangeName) {
		try {
			OdfElement contentRoot = maOwnerTable.mDocument.getContentRoot();
			//create name range element
			OdfFileDom contentDom = ((OdfFileDom) maOwnerTable.getOdfElement().getOwnerDocument());
			TableNamedExpressionsElement nameExpress = (TableNamedExpressionsElement) OdfXMLFactory.newOdfElement(
					contentDom,
					OdfName.newName(OdfDocumentNamespace.TABLE, "named-expressions"));
			String startCellRange = "$" + maOwnerTable.getTableName() + "." + maOwnerTable.getAbsoluteCellAddress(mnStartColumn, mnStartRow);
			String endCellRange = "$" + maOwnerTable.getTableName() + "." + maOwnerTable.getAbsoluteCellAddress(mnEndColumn, mnEndRow);
			TableNamedRangeElement nameRange = (TableNamedRangeElement) nameExpress.newTableNamedRangeElement(startCellRange + ":" + endCellRange, cellRangeName);
			nameRange.setTableBaseCellAddressAttribute(endCellRange);
			contentRoot.appendChild(nameExpress);
			msCellRangeName = cellRangeName;
		} catch (Exception ex) {
			Logger.getLogger(CellRange.class.getName()).log(Level.SEVERE, null, ex);
		}

	}

	/**
	 * Get the <code>Table</code> instance who contains this cell range.
	 * @return the table that contains the cell range.
	 */
	public Table getTable() {
		return maOwnerTable;
	}

	/**
	 * Get the number of rows in this cell range.
	 * @return rows number in the cell range
	 */
	public int getRowNumber() {
		return (mnEndRow - mnStartRow + 1);
	}

	/**
	 * Get the number of columns in this cell range.
	 * @return columns number in the cell range
	 */
	public int getColumnNumber() {
		return (mnEndColumn - mnStartColumn + 1);
	}

	/** 
	 * Returns a single cell that is positioned at specified column and row.
	 * @param clmIndex	the column index of the cell inside the range.
	 * @param rowIndex	the row index of the cell inside the range.
	 * @return
	 * 				the cell at the specified position relative to the start position of the cell range
	 * @throws IndexOutOfBoundsException if the column/row index is bigger than the column/row count
	 */
	public Cell getCellByPosition(int clmIndex, int rowIndex) throws IndexOutOfBoundsException {
		return maOwnerTable.getCellByPosition(mnStartColumn + clmIndex, mnStartRow + rowIndex);
	}

	/**
	 * Check if the given column in is this cell range.
	 * @param colIndex	
	 * 					the given column index
	 * @return true if the given column index is in the current cell range
	 * 
	 */
	private boolean isColumnInCellRange(int colIndex) {
		if (colIndex < mnStartColumn || colIndex > mnEndColumn) {
			return false;
		} else {
			return true;
		}
	}

	/** 
	 * Returns a single cell that is positioned at specified cell address.
	 * 
	 * @param address
	 * 				the cell address of the cell inside the range.
	 * @return
	 * 				the cell at the specified cell address relative to the start position of the cell range
	 */
	public Cell getCellByPosition(String address) {
		//if the address also contain the table name,  but the table is not the maOwnerTable
		//what should do? get the table then getcellByPosition?
		return getCellByPosition(maOwnerTable.getColIndexFromCellAddress(address), maOwnerTable.getRowIndexFromCellAddress(address));
	}
}
